MALHAWK Logo

MalHawk

Reverse Engineering
PLAT:Windows
DIFF:2.5
QUAL:4.3
2026-07-25

where is the challange?

This is my easiest crackme so far but i think its fun so good luck and i hope you can learn something new from it.

DIE Scan Showing PE Overlay

Learnt a hard lesson with this one. I was getting so stressed trying to figure out what was going on, it turned out the binary was packed, and I had no idea at first!. So I analyzed Layer 1, which required an input file on disk containing data. The file size could not be less than 4096 bytes (0x1000). It involved file rewriting: every time the binary ran, it subtracted 4096 bytes from the file. If the remaining data fell short of 4096 bytes, it simply output not found.

After staring at this file-rewriting loop, I realized I was going nowhere with it and wondered: Is this really the end of the binary? Am I supposed to extract something more from it? What is the end goal here?. So I passed it through Detect It Easy (DIE) hoping to get more info about the file, and there it was.. an embedded PE hidden inside an overlay at offset 0x7E00!

DIE Scan Showing PE Overlay

An overlay in a binary is extra data appended to the end of a binary, past its normal sections and headers. There was an entire second 64-bit executable inside the main binary's overlay.

Once I identified that Layer 1 was purely a loader designed to process and unpack payload streams, the goal became clear: extract the embedded binary directly from the overlay!

I extracted the overlay payload by dumping the 0x4E00 bytes starting at offset 0x7E00 using HxD into a new binary: overlay.exe.

IDA Strings Window IDA Strings Window

Scanning through the strings, there was no obvious plain-text clues - just that the file needed input passed and some WINAPI calls checking for debuggers and terminating processes. Instead, we see standard C++ runtime error messages (bad allocation, string too long), system DLL functions, and leftover PDB debug symbols from the author (easycrackme.pdb).

This meant the success and failure messages were being dynamically decrypted or constructed at runtime to prevent simple string searches from spoiling the challenge. Jumping into the main disassembly, we find where the program starts interacting with the console to get input.

Console Input Handling

The program loads std::cin (?cin@std@@3V?$basic_istream...) into RCX and calls the stream input function to read the user's password from the console. If no input is provided, execution branches to load "no input\n" into RDX and prints it to std::cout. Otherwise, execution flows down into loc_7FF6028413E7 and towards the core validation logic.

Decryption Block

Validation and Decryption Loop

It pulls out each character of our entered password, XORs it with a fixed key of 0x50, and compares the result against [rcx+rsi]. Looking at RSI in the registers tab, we can see the encrypted target password string: "123abcqq". If any character does not match, it branches to the SAD PATH (jnz loc_7FF602841571).

Based on standard XOR rules: A XOR B = C -> C XOR B = A

Since the key is fixed at 0x50, we can simply XOR each character of the encrypted target string "123abcqq" with 0x50 to get our password! For example, the first character '1' (0x31) XOR 0x50 gives 0x61 ('a').

Passgen

I wrote a small Python script to automate the decryption of the encrypted password. Our key is set at 0x50.

reference = ["1", "2", "3", "a", "b", "c", "q", "q"]
password_chars = []

for char in reference:
    ascii_value = ord(char)
    xored_value = ascii_value ^ 0x50
    password_chars.append(chr(xored_value))

password = "".join(password_chars)
print(f"password: {password}") # Outputs: abc123!!

Keygen Output

Breakdown:

'1' (0x31) ^ 0x50 = 'a' (0x61)
'2' (0x32) ^ 0x50 = 'b' (0x62)
'3' (0x33) ^ 0x50 = 'c' (0x63)
'a' (0x61) ^ 0x50 = '1' (0x31)
'b' (0x62) ^ 0x50 = '2' (0x32)
'c' (0x63) ^ 0x50 = '3' (0x33)
'q' (0x71) ^ 0x50 = '!' (0x21)
'q' (0x71) ^ 0x50 = '!' (0x21)

If we pass the correct password abc123!!, the program branches past the validation loop to loc_7FF6AFF71440.

Success Message Decryption

Here we see why "correct pass!" was missing from the static strings table earlier! The program loads an encrypted string at r15 ("3?\"\"53$p 1##q"), loops through it, and XORs each byte with 0x50 (xor dl, 50h) right before printing each character to std::cout.

Challenge Solved